
#ifndef TRANSFER_HPP
#define TRANSFER_HPP

#include "common.hpp"
#include "net.hpp"

#include <cstdlib>
#include <cstring>
#include <sys/time.h>
#include <iostream>

extern "C" {
#include <stdint.h>
#include <unistd.h>
}

#include <QtWiAux>


///////////////////
// Data transfer //
///////////////////

/*
 * General description of data transfer:
 *
 *   A data transferer is a class that basically maintains a file descriptor.
 * From a data transferred there are derived data sources and data sinks (destinations).
 * Sources open and prepare the file descriptor for reading, sinks prepare the
 * file descriptor for writing.
 *   For instance, there are TCP and File sources which all have some specific
 * way of preparing a DataTransferrer's file descriptor for reading.
 * Similarily, there are also TCP and File sinks which open the file
 * descriptor of the DataTransferer base class (from which DataSource and DataSink are derived)
 * for writing.
 *   The GuiConnector in items.hpp maintains a Connector class (see further down in this file).
 * Upon initialization of a specific connection, data sources and sinks are created. By means
 * the the generic Connector classes their file descriptors are now connected and
 * the Connector begins to "pump" bytes from source to sink, no matter what kind
 * of source and sink have been selected.
 * Meanwhile the Connector does also measure the data rate depending on the current chunk
 * size opted for.
 *   DataTransferers have their way of reporting information and measures to Gauges (see gauges.hpp).
 * Connector Gui has a timeout method which gets invoked periodically. Therein
 * the Connector Gui queries information from sources and sinks using the GaugeInfo struct
 * and reports it to the specific Gauges.
 * For almost every sink and source there are corresponding gauges.
 */


struct GaugeInfo
{
	
};


class DataTransferer : public ErrorReporter
{
private:

	mutable volatile int fd;
	IMutexLock<int, PTHREAD_MUTEX_RECURSIVE> fd_lock;

	volatile bool is_ready;
	IMutexLock<bool> is_ready_lock;

protected:

	void setReady(bool val)
	{
		is_ready_lock.set(val);
	}

	void setFd(int data_fd)
	{
		fd = data_fd;
	}
	
public:

	DataTransferer() : fd(-1), fd_lock(fd), is_ready(false), is_ready_lock(is_ready)
	{ }

	// Prepare information for TCP gauge; usually GuiConnector calls this method to
	// pick info up and to then deliver it to the Gauges
        virtual GaugeInfo& getGaugeInfo() = 0;

	/*
	  todo:

	~DataTransferer()
	{
		close(fd);
	}
	*/

	IMutexLock<int, PTHREAD_MUTEX_RECURSIVE>& getFdLock()
	{
		return fd_lock;
	}
	
	volatile int getFd() const
	{
		return fd;
	}

	volatile int& getFdRef()
	{
		return fd;
	}
	
	bool isReady()
	{
		return is_ready_lock.read();
	}

	// After calling the follwing function a caller ought to check if errors occurred using
	// ErrorReporter::hasErrors()
	
	virtual void getReady() = 0;
};

class DataSource : virtual public DataTransferer
{

};

class DataDestination : virtual public DataTransferer
{

};

///////// TCP ///////////

extern "C" {
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
}


struct TCPGaugeInfo : GaugeInfo
{
	double rtt;
};

extern Ping pinger;

// TCP Transferer
class TCPtransferer : virtual public DataTransferer
{
protected:
	struct sockaddr_in remote;
	uint16_t port;

public:
	TCPtransferer(uint16_t p) : port(p) { }
	
	static std::string myIP()
	{
		// todo
		return std::string();
	}

	std::string connectedWithIP() const
	{
		// todo: non-portable inet_ntoa ?
		return std::string(inet_ntoa(remote.sin_addr));
	}
	
	GaugeInfo& getGaugeInfo()
	{
		TCPGaugeInfo *g = new TCPGaugeInfo();

		// Retrieve round-trip-time
		g->rtt = pinger.getRtt();

		return *g;
	}

protected:
	
	// todo: return status
	static int prepare(volatile int &sockfd, uint16_t &port, struct sockaddr_in *sa);

};


// TCP destination (client)

class TCPdestination : public TCPtransferer, public DataDestination
{
private:
	std::string remote_ip;
public:

	// todo: construcotr ip
	TCPdestination(const char *ip, uint16_t port) : TCPtransferer(port), remote_ip(ip)
	{ }
	
	virtual void getReady();
};


// TCP source (server)

class TCPsource : private TCPtransferer, public DataSource, private IThread
{
private:
	int server_fd;
	struct sockaddr_in local;
	static const int backlog;
	
public:

	TCPsource(uint16_t port) : TCPtransferer(port)
	{ }
	
	virtual void getReady();

	// Connection accepter thread
	void action(void *);

	/*
	  todo:
	  ~TCPsource()
	  {
	*/
};


////////// File  /////////

extern "C" {
#include <sys/stat.h>
#include <fcntl.h>
}

struct FileGaugeInfo : GaugeInfo
{
	std::string path;
	IMutexLock<int, PTHREAD_MUTEX_RECURSIVE> &fd_lock;

	FileGaugeInfo(IMutexLock<int, PTHREAD_MUTEX_RECURSIVE> &f) : fd_lock(f)
	{ }
};

class FileTransferer : virtual public DataTransferer
{
protected:
	std::string path;
	int openflags;
	
public:
	FileTransferer(std::string p, int flags) : path(p), openflags(flags) 
	{ }

	void open_fd()
	{
		int fd;
		if((fd = open(const_cast<char*>(path.c_str()), openflags, 0666)) < 0)
			ErrorReporter::error(ErrorReporter::RECOVERABLE, "Unable to open file " + path);

		status("Opened successfully: " + path);

		std::cout << "FD: " << fd << std::endl;
		
		setFd(fd);
		DataTransferer::setReady(true);
	}
				    
	virtual void getReady()
	{
		ErrorReporter::recover();
		if(isReady()) return;
		open_fd();
	}

	GaugeInfo& getGaugeInfo()
	{
		FileGaugeInfo &fgi = *new FileGaugeInfo(getFdLock());
		fgi.path = path;
		return fgi;
		
	}
};

class FileSource : public DataSource, public FileTransferer
{
public:
	FileSource(std::string path) : FileTransferer(path, O_RDONLY)
	{ }
};

class FileDestination : public DataDestination, public FileTransferer
{
public:
	FileDestination(std::string path) : FileTransferer(path, O_WRONLY | O_CREAT | O_TRUNC)
	{ }
};

// todo: interface here, single impelentations in single source files like tcp.cpp


//////////////// Pipe /////////////////

class PipeTransferer : protected FileTransferer
{

public:
	PipeTransferer(std::string pipe, int oflags) : FileTransferer(pipe, oflags)
	{ }

	virtual void getReady()
	{
		//todo:  if exists then ok
		if((mkfifo(path.c_str(), 0777)) < 0 && errno != EEXIST) {
			ErrorReporter::error(ErrorReporter::RECOVERABLE, "Failed to create pipe " + path);
			return;
		}
		
		FileTransferer::getReady();
	}
};

class PipeSource : public PipeTransferer, public DataSource
{
public:

	PipeSource(std::string pipe) : PipeTransferer(pipe, O_RDONLY)
	{ }
};

class PipeDestination : public PipeTransferer, public DataDestination
{
public:

	PipeDestination(std::string pipe) : PipeTransferer(pipe, O_WRONLY)
	{ }
};

///////////// Serial /////////////////

struct SerialGaugeInfo : GaugeInfo
{
	IMutexLock<int, PTHREAD_MUTEX_RECURSIVE> &fd_lock;
	int fd;
	
	SerialGaugeInfo(IMutexLock<int, PTHREAD_MUTEX_RECURSIVE> &fd_lock, int fd) : fd_lock(fd_lock), fd(fd)
	{ }
};

class SerialTransferer :  public FileTransferer
{
	
public:

	SerialTransferer(std::string device, int oflags) : FileTransferer(device, oflags | O_NOCTTY)
	{
		FileTransferer::open_fd();
	}
	
	SerialGaugeInfo& getGaugeInfo()
	{
		return *new SerialGaugeInfo(getFdLock(), getFd());
	}
};

class SerialSource : public SerialTransferer, public DataSource
{
public:
	SerialSource(std::string serial) : SerialTransferer(serial, O_RDONLY)
	{ }

};

class SerialDestination : public SerialTransferer, public DataDestination
{
public:
	SerialDestination(std::string serial) : SerialTransferer(serial, O_WRONLY)
	{ }
};

/*
 * ConnectorGui holds one Connector.
 * This class is responsible for copying data from fd_source to
 * fd_dest while measuring the data rate which can be obtain
 * by calling the class' getCurrentRate() method. This method
 * is inherintely thread-safe as Connector itself is a implemented
 * as an IThread for the actual process of copying data between
 * source and destination file descriptors.
 * Furthermore, the chunk size, that is the size of data that
 * is attempted to be copied in one step to measure the data
 * rate, can be set variably also by using setChunkSize().
 * Naturally, this method is also thread-safe.
 *
 */
class Connector : public ErrorReporter, public IThread
{
private:
	
	DataSource *source;
	DataDestination *dest;

	volatile int chunk_size;
	volatile int random_bulkiness_delay;
	volatile long double data_rate;
	volatile long int total_transferred;

	IMutexLock<int> chunk_size_lock;
	IMutexLock<int> random_bulkiness_delay_lock;
	IMutexLock<long double> data_rate_lock;
	IMutexLock<long int> total_transferred_lock;

	volatile uint32_t s11n;
	IMutexLock<uint32_t> s11n_lock;

	
	/*
	 * Read chunk_size bytes from source file descriptor as supplied by DataSource
	 * and write them to the the file descriptor supplied by DataDestination.
	 */

	enum PumpStatus {
		
		PUMPABLE,
		EMPTY,
		ERROR
	};
	
	inline PumpStatus pump_chunk();
	
public:

	// todo: make Connector use Timer class from aux.hpp also
	int timer(struct timeval *elapsed) const;

	static const int default_chunk_size;
	static const int max_chunk_size;
	static const int max_delay_time;
	static const int connection_timeout;
	
	Connector(DataSource *s, DataDestination *d) :
		source(s), dest(d),
		chunk_size(default_chunk_size), random_bulkiness_delay(0), data_rate(0), total_transferred(0),
		chunk_size_lock(chunk_size), random_bulkiness_delay_lock(random_bulkiness_delay),
		data_rate_lock(data_rate), total_transferred_lock(total_transferred),
		s11n_lock(s11n)
	{ }

	~Connector()
	{
		// todo: this segfaults
		//delete source;
		//delete dest;
	}

	uint32_t getS11n()
	{
		return s11n_lock.read();
	}


	static std::string unit_convert(long double v, bool byte)
	{
		std::string rstr("0 bit");

		long double a = v, b;
		
		if((b = a / 1024) < 1) {
			rstr = to_string<long double>(rndn(a, 2)) + " ";
		} else if((a = b / 1024) < 1) {
			rstr = to_string<long double>(rndn(b, 2)) + " k";
		} else if((b = a / 1024) < 1) {
			rstr = to_string<long double>(rndn(a, 2)) + " M";
		} else if(v)
			rstr = to_string<long double>(rndn(b, 2)) + " G";

		rstr += byte ? "byte" : "bit";
		
		return rstr;
	}
	
	static inline std::string bitRateString(long double bits_per_s)
	{
		return unit_convert(bits_per_s, false) + "/s"; 
	}
	

	void setChunkSize(int size)
	{
		chunk_size_lock.set(size);
	}

	void setMaxDelay(int dmax)
	{
		random_bulkiness_delay_lock.set(dmax);
	}

	long double getDataRate()
	{
		return data_rate_lock.read();
	}

	long int getTotalBytes()
	{
		return total_transferred_lock.read();
	}
	
	bool have_errors()
	{
		if(source->hasErrors()) {
			ErrorReporter::mirror(*source);
			return true;
		}

		if(dest->hasErrors()) {
			ErrorReporter::mirror(*dest);
			return true;
		}

		return false;
	}

	virtual void action(void *);

	bool isPumping();
};



#endif
